<?php

/**
 * Maikuro CMS
 *
 * LICENSE:
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * The license is bundled with this package in the file LICENSE.txt.
 * You may also obtain a copy of the License at this URL:
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @copyright  Copyright (c) 2010, Jonathan Hedrén
 * @author     Jonathan Hedrén <jonathan.hedren@gmail.com>
 * @license    http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
 */

/**
 * @see AttributeParser
 */
require_once 'attributes/AttributeParser.php';

/**
 * @see PersistentAttribute
 */
require_once 'PersistentAttribute.php';

abstract class AbstractDbEntityService
{
    /**
     * @var ICacheHandler
     */
    protected $cacheHandler;

    /**
     * @var string
     */
    protected $cacheIdentifier;

    /**
     * @var ExtendedPDO
     */
    protected $pdo;

    /**
     * @var string
     */
    protected $dbTable;

    /**
     *
     * @var array
     */
    protected $entities = array();

    /**
     * @var string
     */
    protected $entityClass;

    /**
     * Constructor. Will load the entities and save them to the cache if they
     * are not already cached.
     *
     * @param ICacheHandler $cacheHandler
     * @param ExtendedPDO $pdo
     * @return AbstractDbEntityService
     */
    public function __construct(ICacheHandler $cacheHandler, ExtendedPDO $pdo)
    {
        if ($this->cacheIdentifier == null) {
            throw new Exception('The cacheIdentifier property must be set.');
        }
        if ($this->dbTable == null) {
            throw new Exception('The dbTable property must be set.');
        }
        if ($this->entityClass == null) {
            throw new Exception('The entityClass property must be set.');
        }

        $this->cacheHandler = $cacheHandler;
        $this->pdo = $pdo;

        $this->loadEntities();
        
        return $this;
    }

    /**
     * Creates an entity.
     * 
     * @param array $data
     * @return mixed
     */
    public function create(array $data = null)
    {
        return $this->createEntity($this->entityClass, $data);
    }

    /**
     * Creates an entity of the specified class with any optional data.
     *
     * @param string $entityClass
     * @param array $data
     * @return mixed
     */
    protected function createEntity($entityClass, array $data = null)
    {
        $entity = new $entityClass();
        if (is_array($data)) {
            foreach ($data as $key => $value) {
                if (!property_exists($entityClass, $key)) {
                    continue;
                }
                $entity->$key = $value;
            }
        }
        return $entity;
    }

    /**
     * Deletes an entity.
     *
     * @param mixed $idOrEntity
     * @return AbstractDbEntityService
     */
    public function delete($idOrEntity)
    {
        if ($idOrEntity instanceof $this->entityClass) {
            $id = $idOrEntity->id;
        } else {
            $id = $idOrEntity;
        }

        $this->pdo->delete($this->dbTable, 'id = ?', $id);

        unset($idOrEntity);
        
        return $this;
    }

    /**
     * Returns the entity with the specified id.
     *
     * @param mixed $id
     * @return mixed
     */
    protected function get($id)
    {
        if (isset($this->entities[$id])) {
            return clone $this->entities[$id];
        }
        return null;
    }

    /**
     *
     * @param string $className
     * @param object $instance
     * @return array
     */
    protected function getColumnsAndValues($className, $instance)
    {
        // Get all columns and values from the user. Only the properties
        // with the Persistent/PersistentAttribute attribute will be saved.
        $columnsAndValues = array();
        foreach (get_class_vars($className) as $property => $value) {
            $attributes = AttributeParser::getPropertyAttributesFor($className, $property);
            if ($attributes == null || !count($attributes)) {
                continue;
            }
            $persistentAttribute;
            foreach ($attributes as $attribute) {
                if ($attribute instanceof PrimaryKeyAttribute) {
                    continue 2;
                }

                // FIXME: Detta funkar ju inte om man har flera attribut på propertyn!
                if ($attribute instanceof PersistentAttribute) {
                    $persistentAttribute = $attribute;
                    break;
                }
            }

            if ($persistentAttribute == null) {
                continue;
            }

            $columnsAndValues[$property] = $instance->$property;
        }
        return $columnsAndValues;
    }

    /**
     * Fetches the entities from the database.
     *
     * @return void
     */
    protected function fetchEntities()
    {
        $result = $this->pdo->select()
                            ->from($this->dbTable)
                            ->fetchAllAs($this->entityClass);
        foreach ($result as $entity) {
            $this->entities[$entity->id] = $entity;
        }
    }

    /**
     * Loads the entities from the cache if possible, else fetches the from the
     * database.
     *
     * @return void
     */
    protected function loadEntities()
    {
        $entities = $this->cacheHandler->load($this->cacheIdentifier);
        if ($entities == null) {
            $this->fetchEntities();
            $this->cacheHandler->save($this->cacheIdentifier, $this->entities);
        } else {
            $this->entities = $entities;
        }
    }

    /**
     * Purges the entities cache.
     *
     * @return boolean
     */
    public function purgeCache()
    {
        return $this->cacheHandler->clean($this->cacheIdentifier);
    }

    /**
     * Saves an entity.
     *
     * @param $entity
     * @return mixed
     */
    public function save($entity)
    {
        if (!$entity instanceof $this->entityClass) {
            throw new Exception('The entity is not an instance of ' . $this->entityClass . '.');
        }

        if ($this->dbTable == null) {
            throw new Exception('Unknown database table.');
        }

        $columnsAndValues = $this->getColumnsAndValues($this->entityClass, $entity);

        // Insert/update the database
        if ($entity->id === null) {
            $result = $this->pdo->insert($this->dbTable, $columnsAndValues);
            // If we've got no affected rows there is a serious problem.
            if ($result < 1) {
                throw new Exception('The database returned zero affected rows which indicates that no data was saved.');
            }
            $entity->id = $this->pdo->lastInsertId();
        } else {
            $this->pdo->update($this->dbTable, $columnsAndValues, 'id = ?', $entity->id);
        }

        return $entity;
    }

    /**
     * Sets the entity class name to use for page entities.
     *
     * @param string $entityClass
     * @return AbstractDbEntityService
     */
    public function setEntityClass($entityClass)
    {
        $this->entityClass = $entityClass;
        return $this;
    }
}